package toman;

import javax.persistence.CascadeType;
import javax.persistence.Temporal;
import javax.persistence.TemporalType;
import java.lang.reflect.*;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Date;
import java.util.List;

/**
 * User: zslajchrt
 * Date: 21.4.11
 * Time: 15:08
 */
public class TOManImpl implements TOMan {

    private final QueryEngineSelector queryEngineSelector;
    private PropertyEvaluator propertyEvaluator = DefaultPropertyEvaluator.INSTANCE;

    public TOManImpl() {
        this(new QueryEngineSelectorImpl());
    }

    public PropertyEvaluator getPropertyEvaluator() {
        return propertyEvaluator;
    }

    public void setPropertyEvaluator(PropertyEvaluator propertyEvaluator) {
        this.propertyEvaluator = propertyEvaluator;
    }

    public QueryEngineSelector getQueryEngineSelector() {
        return queryEngineSelector;
    }

    public TOManImpl(QueryEngineSelector queryEngineSelector) {
        this.queryEngineSelector = queryEngineSelector;
    }

    public <T, K> T najdiTO(Class<T> toClass, K toId) {
        return findTO(toClass, toId);
    }

    public <T, K> T findTO(Class<T> toClass, K toId) {
        Object row = findInitObject(toClass, toId);
        return createTOInstance(toClass, row);
    }

    private <T, K> Object findInitObject(Class<T> toClass, K toId) {
        QueryEngine queryEngine = queryEngineSelector.selectEngineForFindInitObject(this, toClass, toId);
        try {
            return queryEngine.findInitObject(this, toClass, toId);
        } catch (Exception e) {
            throw handleException(e);
        }
    }

    private RuntimeException handleException(Exception e) {
        if (e instanceof RuntimeException)
            return (RuntimeException) e;
        else
            return new IllegalArgumentException(e);
    }

    public <T> TOQuery<T> createContextQuery(Class<T> toClass, Object condition, Object context) {
        resolvePossibleExpression(condition, context);

        QueryEngine queryEngine = queryEngineSelector.selectEngineForCreateQuery(this, toClass, condition);
        try {
            TOQuery<T> query = queryEngine.createQuery(this, toClass, condition);
            setQueryParameters(query, context, propertyEvaluator);
            return query;
        } catch (Exception e) {
            throw handleException(e);
        }
    }

    public <T> TOQuery<T> createQuery(Class<T> toClass, Object condition) {
        QueryEngine queryEngine = queryEngineSelector.selectEngineForCreateQuery(this, toClass, condition);
        try {
            return queryEngine.createQuery(this, toClass, condition);
        } catch (Exception e) {
            throw handleException(e);
        }
    }

    // common
    public <T> T createTOInstance(Class<T> toClass, Object row) {
        try {
            if (row == null)
                return null;

            T to;
            to = toClass.newInstance();

            // todo: call @PostConstruct - it allows initializing the TO properties that will be used for eventual
            // todo: evaluating expressions in the TO annotation.
            // todo: Syntax: @PostConstruct void init(TOMan toman, Map<String, Object> hints)
            // postConstruct.invoke(to, this, hints(?))

            List<Member> refMembers = new ArrayList<Member>();

            injectValues(toClass, row, to, refMembers);

            // resolve context queries for ref fields
            injectRefs(to, refMembers);

            return to;
        } catch (Exception e) {
            throw handleException(e);
        }
    }

    private <T> void injectValues(Class<T> toClass, Object row, T to, List<Member> refMembers) throws IllegalAccessException, InvocationTargetException {

        injectValuesToMethods(toClass, row, to, refMembers);

        injectValuesToFields(toClass, row, to, refMembers);

    }

    private <T> void injectValuesToMethods(Class<T> toClass, Object row, T to, List<Member> refMembers) throws IllegalAccessException, InvocationTargetException {
        for (Method m : toClass.getMethods()) {
            Index indAnnot = m.getAnnotation(Index.class);
            if (indAnnot != null && row instanceof Object[]) {
                Object[] entityAttrs = (Object[]) row;
                m.invoke(to, entityAttrs[indAnnot.value()]);
            } else {
                Prop propAnnot = m.getAnnotation(Prop.class);
                if (propAnnot != null && !(row instanceof Object[])) {
                    String propertyName = propAnnot.value();
                    if ("".equals(propertyName)) {
                        propertyName = convertMethodNameToPropertyName(m.getName());
                    }
                    Object propertyValue = getPropertyValueFromPath(propertyName, row);
                    m.invoke(to, propertyValue);
                } else {
                    Ref refAnnot = m.getAnnotation(Ref.class);
                    if (refAnnot != null) {
                        refMembers.add(m);
                    }
                }
            }
        }
    }

    private <T> void injectValuesToFields(Class<T> toClass, Object row, T to, List<Member> refMembers) throws IllegalAccessException {

        for (Field f : toClass.getDeclaredFields()) {
            Index indAnnot = f.getAnnotation(Index.class);
            if (indAnnot != null && row instanceof Object[]) {
                Object[] entityAttrs = (Object[]) row;
                f.setAccessible(true);
                f.set(to, entityAttrs[indAnnot.value()]);
            } else {
                Prop propAnnot = f.getAnnotation(Prop.class);
                if (propAnnot != null && !(row instanceof Object[])) {
                    String propertyName = propAnnot.value();
                    if ("".equals(propertyName)) {
                        propertyName = f.getName();
                    }
                    Object propertyValue = getPropertyValueFromPath(propertyName, row);
                    f.setAccessible(true);
                    f.set(to, propertyValue);
                } else {
                    Ref refAnnot = f.getAnnotation(Ref.class);
                    if (refAnnot != null) {
                        refMembers.add(f);
                    }
                }
            }
        }

        if (toClass.getSuperclass() != Object.class) {
            injectValuesToFields(toClass.getSuperclass(), row, to, refMembers);
        }
    }

    private <T> void injectRefs(T to, List<Member> refMembers) throws IllegalAccessException, InvocationTargetException {
        for (Member refMember : refMembers) {
            boolean isField = refMember instanceof Field;

            Field refField = isField ? (Field) refMember : null;
            Method refMethod = isField ? null : (Method) refMember;

            Ref refAnnot = isField ? refField.getAnnotation(Ref.class)
                    : refMethod.getAnnotation(Ref.class);

            Class<?> refType = isField ? refField.getType()
                    : refMethod.getParameterTypes()[0];

            Type refGenType = isField ? refField.getGenericType()
                    : refMethod.getGenericParameterTypes()[0];

            String condition = refAnnot.value();
            Class<?> refTOClass;
            Object ref;
            if (Collection.class.isAssignableFrom(refType)) {
                // list result
                // todo: check the type properly and throw an exception if it does not conform the rules
                refTOClass = (Class) ((ParameterizedType) refGenType).getActualTypeArguments()[0];

                TOQuery refQuery = createContextQuery(refTOClass, condition, to);
                ref = refQuery.getResults();
            } else {
                // single result
                refTOClass = refType;

                TOQuery refQuery = createContextQuery(refTOClass, condition, to);
                ref = refQuery.getSingleResult();
            }

            if (isField) {
                refField.setAccessible(true);
                refField.set(to, ref);
            } else {
                refMethod.invoke(to, ref);
            }
        }
    }

    private Object getMemberValue(Member m, Object context) {
        try {
            if (m instanceof Field) {
                Field field = (Field) m;
                field.setAccessible(true);
                return field.get(context);
            } else {
                return ((Method)m).invoke(context);
            }
        } catch (IllegalAccessException e) {
            throw new IllegalArgumentException(e);
        } catch (InvocationTargetException e) {
            throw new IllegalArgumentException(e);
        }
    }

    interface RefFilter {
        boolean filter(Ref ref);
    }

    private List<Member> findAllRefs(Object to, RefFilter filter) {
        return findAllRefs(to, new ArrayList<Member>(), to.getClass(), filter);
    }

    private List<Member> findAllRefs(Object to, List<Member> refFields, Class cls, RefFilter filter) {
        for (Field field : cls.getDeclaredFields()) {
            Ref ref = field.getAnnotation(Ref.class);
            if (ref != null && (filter == null || filter.filter(ref))) {
                refFields.add(field);
            }
        }

        if (cls.getSuperclass() != Object.class) {
            findAllRefs(to, refFields, cls.getSuperclass(), filter);
        }

        return refFields;
    }

    private static String convertMethodNameToPropertyName(String methodName) {
        if (methodName.startsWith("get")) {
            String propName = methodName.substring("get".length());
            return Character.toLowerCase(propName.charAt(0)) + propName.substring(1);
        } else {
            return methodName;
        }

    }

    private Object getPropertyValueFromPath(String propertyPath, Object obj) {
        String[] steps = propertyPath.split("\\.");
        Object context = obj;
        for (String step : steps) {
            context = propertyEvaluator.getPropertyValue(context, step);
            if (context == null) {
                return null;
            }
        }

        return context;
    }

    public <T> void ulozTO(T to) throws Exception {
        updateTO(to);
    }

    private static class MergeCascadeTypeFilter implements RefFilter {
        public boolean filter(Ref ref) {
            for (CascadeType cascadeType : ref.cascade()) {
                if (cascadeType == CascadeType.ALL || cascadeType == CascadeType.MERGE) {
                    return true;
                }
            }
            return false;
        }
    }

    private MergeCascadeTypeFilter mergeCascadeTypeFilter = new MergeCascadeTypeFilter();

    public <T> void updateTO(T to) {
        QueryEngine queryEngine = queryEngineSelector.selectEngineForCreateQueryForUpdate(this, to);
        try {
            QueryForUpdate queryForUpdate = queryEngine.createQueryForUpdate(this, to);
            setQueryParameters(queryForUpdate, to, propertyEvaluator);
            queryForUpdate.executeUpdate();

            // cascade
            List<Member> updateRefs = findAllRefs(to, mergeCascadeTypeFilter);
            for (Member updateRef : updateRefs) {
                Object refValue = getMemberValue(updateRef, to);
                if (refValue instanceof List) {
                    List<?> refs = (List)refValue;
                    for (Object ref : refs) {
                        this.updateTO(ref);
                    }
                } else {
                    this.updateTO(refValue);
                }
            }

        } catch (Exception e) {
            throw handleException(e);
        }
    }

    protected Object resolveTemporalType(Object context, String propName, PropertyEvaluator propEval) {
        Temporal tempAnnot = propEval.getPropertyAnnotation(context, Temporal.class, propName);
        return tempAnnot == null ? null : tempAnnot.value();
    }

    public Object resolvePossibleExpression(Object conditionDef, Object context) {
        if (conditionDef instanceof String) {
            String conditionExpr = (String) conditionDef;
            if (conditionExpr.startsWith("${")) {
                String propertyPath = conditionExpr.substring(2, conditionExpr.length() - 1);
                return getPropertyValueFromPath(propertyPath, context);
            } else {
                return conditionExpr; // constant
            }
        } else {
            return conditionDef;
        }
    }

    private void setQueryParameters(ParameterizedQuery query, Object context, PropertyEvaluator propEval) {
        for (String paramName : query.getParameterNames()) {
            setQueryParameter(query, paramName, context, propEval);
        }
    }

    private void setQueryParameter(ParameterizedQuery query, String paramName, Object context,
                                   final PropertyEvaluator propEval) {
        final Object[] temporalType = new Object[1];
        Object paramValue = propEval.getPropertyValue(context, paramName);

        temporalType[0] = resolveTemporalType(context, paramName, propEval);

        if (temporalType[0] == null) {
            query.setParameter(paramName, paramValue);
        } else {
            query.setParameter(paramName, (Date) paramValue, (TemporalType) temporalType[0]);
        }
    }

}